/*
 * This file is part of Dependency-Track.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 * Copyright (c) OWASP Foundation. All Rights Reserved.
 */
package org.dependencytrack.model;

import alpine.common.validation.RegexSequence;
import alpine.server.json.TrimmedStringDeserializer;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import org.dependencytrack.parser.common.resolver.CweResolver;
import org.dependencytrack.persistence.CollectionIntegerConverter;
import org.dependencytrack.resources.v1.serializers.CweDeserializer;
import org.dependencytrack.resources.v1.serializers.CweSerializer;
import org.dependencytrack.resources.v1.serializers.Iso8601DateSerializer;
import org.dependencytrack.resources.v1.vo.AffectedComponent;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import javax.jdo.annotations.Column;
import javax.jdo.annotations.Convert;
import javax.jdo.annotations.Extension;
import javax.jdo.annotations.FetchGroup;
import javax.jdo.annotations.FetchGroups;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.Index;
import javax.jdo.annotations.Order;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import javax.jdo.annotations.Unique;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

/**
 * Model for tracking vulnerabilities.
 *
 * @author Steve Springett
 * @since 3.0.0
 */
@PersistenceCapable
@FetchGroups({
        @FetchGroup(name = "COMPONENTS", members = {
                @Persistent(name = "components")
        }),
        @FetchGroup(name = "METRICS_UPDATE", members = {
                @Persistent(name = "vulnId"),
                @Persistent(name = "source"),
                @Persistent(name = "severity"),
                @Persistent(name = "cvssV2BaseScore"),
                @Persistent(name = "cvssV3BaseScore")
        }),
        @FetchGroup(name = "VULNERABLE_SOFTWARE", members = {
                @Persistent(name = "vulnerableSoftware")
        })
})
@JsonInclude(JsonInclude.Include.NON_NULL)
@Unique(members = {"vulnId", "source"})
public class Vulnerability implements Serializable {

    private static final long serialVersionUID = -3002699553847728904L;

    /**
     * Defines the JDO fetch groups for this class.
     */
    public enum FetchGroup {
        COMPONENTS,
        METRICS_UPDATE,
        VULNERABLE_SOFTWARE,
    }

    /**
     * Defines the sources of vulnerability data supported by Dependency-Track.
     */
    public enum Source {
        NVD,      // National Vulnerability Database
        NPM,      // NPM Public Advisories - NEED TO KEEP FOR LEGACY PURPOSES
        GITHUB,   // GitHub Security Advisories
        VULNDB,   // VulnDB from Risk Based Security
        OSSINDEX, // Sonatype OSS Index
        RETIREJS, // Retire.js
        INTERNAL, // Internally-managed (and manually entered) vulnerability
        OSV,      // Google OSV Advisories
        SNYK,     // Snyk
        TRIVY,    // Trivy
        UNKNOWN;  // Unknown vulnerability sources

        public static boolean isKnownSource(String source) {
            return Arrays.stream(values()).anyMatch(enumSource -> enumSource.name().equalsIgnoreCase(source));
        }

        public static Source resolve(String id) {
            if (id.startsWith("CVE-")){
                 return NVD;
            } else if (id.startsWith("GHSA-")){
                return GITHUB;
            } else if (id.startsWith("OSV-")){
                return OSV;
            } else if (id.startsWith("SNYK-")){
                return SNYK;
            }

            return UNKNOWN;
        }
    }

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE)
    @JsonIgnore
    private long id;

    @Persistent
    @Column(name = "VULNID", allowsNull = "false")
    @Index(name = "VULNERABILITY_VULNID_IDX")
    @NotBlank
    @Size(min = 1, max = 255)
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The vulnerability ID may only contain printable characters")
    private String vulnId;

    @Persistent
    @Column(name = "SOURCE", allowsNull = "false")
    @NotBlank
    @Size(min = 1, max = 255)
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The source may only contain printable characters")
    private String source;

    @Persistent
    @Column(name = "FRIENDLYVULNID")
    @NotBlank
    @Size(min = 1, max = 255)
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The friendly vulnerability ID may only contain printable characters")
    private String friendlyVulnId;

    @Persistent
    @Column(name = "TITLE")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The title may only contain printable characters")
    private String title;

    @Persistent
    @Column(name = "SUBTITLE")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The subtitle may only contain printable characters")
    private String subTitle;

    @Persistent
    @Column(name = "DESCRIPTION", jdbcType = "CLOB")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The description may only contain printable characters")
    private String description;

    @Persistent
    @Column(name = "DETAIL", jdbcType = "CLOB")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The detail may only contain printable characters")
    private String detail;

    @Persistent
    @Column(name = "RECOMMENDATION", jdbcType = "CLOB")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The recommendation may only contain printable characters")
    private String recommendation;

    @Persistent
    @Column(name = "REFERENCES", jdbcType = "CLOB")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The references may only contain printable characters")
    private String references;

    @Persistent
    @Column(name = "CREDITS", jdbcType = "CLOB")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The credits may only contain printable characters")
    private String credits;

    @Persistent
    @Column(name = "CREATED")
    @Index(name = "VULNERABILITY_CREATED_IDX")
    @JsonSerialize(using = Iso8601DateSerializer.class)
    private Date created;

    @Persistent
    @Column(name = "PUBLISHED")
    @Index(name = "VULNERABILITY_PUBLISHED_IDX")
    @JsonSerialize(using = Iso8601DateSerializer.class)
    private Date published;

    @Persistent
    @Column(name = "UPDATED")
    @Index(name = "VULNERABILITY_UPDATED_IDX")
    @JsonSerialize(using = Iso8601DateSerializer.class)
    private Date updated;

    @Persistent(defaultFetchGroup = "true")
    @Column(name = "CWES")
    @Convert(CollectionIntegerConverter.class)
    @JsonSerialize(using = CweSerializer.class)
    @JsonDeserialize(using = CweDeserializer.class)
    @ArraySchema(schema = @Schema(implementation = Cwe.class))
    private List<Integer> cwes;

    @Persistent
    @Column(name = "CVSSV2BASESCORE", scale = 1)
    private BigDecimal cvssV2BaseScore;

    @Persistent
    @Column(name = "CVSSV2IMPACTSCORE", scale = 1)
    private BigDecimal cvssV2ImpactSubScore;

    @Persistent
    @Column(name = "CVSSV2EXPLOITSCORE", scale = 1)
    private BigDecimal cvssV2ExploitabilitySubScore;

    @Persistent
    @Column(name = "CVSSV2VECTOR")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The CVSSv2 Vector may only contain printable characters")
    private String cvssV2Vector;

    @Persistent
    @Column(name = "CVSSV3BASESCORE", scale = 1)
    private BigDecimal cvssV3BaseScore;

    @Persistent
    @Column(name = "CVSSV3IMPACTSCORE", scale = 1)
    private BigDecimal cvssV3ImpactSubScore;

    @Persistent
    @Column(name = "CVSSV3EXPLOITSCORE", scale = 1)
    private BigDecimal cvssV3ExploitabilitySubScore;

    @Persistent
    @Column(name = "CVSSV3VECTOR")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The CVSSv3 Vector may only contain printable characters")
    private String cvssV3Vector;

    @Persistent
    @Column(name = "OWASPRRLIKELIHOODSCORE", scale = 1)
    private BigDecimal owaspRRLikelihoodScore;

    @Persistent
    @Column(name = "OWASPRRTECHNICALIMPACTSCORE", scale = 1)
    private BigDecimal owaspRRTechnicalImpactScore;

    @Persistent
    @Column(name = "OWASPRRBUSINESSIMPACTSCORE", scale = 1)
    private BigDecimal owaspRRBusinessImpactScore;

    @Persistent
    @Column(name = "OWASPRRVECTOR")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The OWASP RR Vector may only contain printable characters")
    private String owaspRRVector;

    @Persistent
    @Column(name = "SEVERITY")
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The severity may only contain printable characters")
    private Severity severity;

    @Persistent
    @Column(name = "VULNERABLEVERSIONS")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The vulnerable versions may only contain printable characters")
    private String vulnerableVersions;

    @Persistent
    @Column(name = "PATCHEDVERSIONS")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The patched versions may only contain printable characters")
    private String patchedVersions;

    @Persistent
    @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC"))
    @JsonIgnore
    private List<VulnerableSoftware> vulnerableSoftware;

    @Persistent
    @Column(name = "EPSSSCORE", scale = 5)
    private BigDecimal epssScore;

    @Persistent
    @Column(name = "EPSSPERCENTILE", scale = 5)
    private BigDecimal epssPercentile;

    @Persistent(mappedBy = "vulnerabilities")
    @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC"))
    private List<Component> components;

    @Persistent(mappedBy = "vulnerabilities")
    @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC"))
    private List<ServiceComponent> serviceComponents;

    @Persistent(customValueStrategy = "uuid")
    @Unique(name = "VULNERABILITY_UUID_IDX")
    @Column(name = "UUID", jdbcType = "VARCHAR", length = 36, allowsNull = "false")
    @NotNull
    private UUID uuid;

    private transient int affectedProjectCount;

    private transient int affectedActiveProjectCount;

    private transient int affectedInactiveProjectCount;

    private transient FindingAttribution findingAttribution;

    private transient List<AffectedComponent> affectedComponents;

    private transient List<VulnerabilityAlias> aliases;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public Severity getSeverity() {
        return severity;
    }

    public void setSeverity(Severity severity) {
        this.severity = severity;
    }

    public String getVulnId() {
        return vulnId;
    }

    public void setVulnId(String vulnId) {
        this.vulnId = vulnId;
    }

    public String getFriendlyVulnId() {
        return friendlyVulnId;
    }

    public void setFriendlyVulnId(String friendlyVulnId) {
        this.friendlyVulnId = friendlyVulnId;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getRecommendation() {
        return recommendation;
    }

    public String getDetail() {
        return detail;
    }

    public void setDetail(String detail) {
        this.detail = detail;
    }

    public void setRecommendation(String recommendation) {
        this.recommendation = recommendation;
    }

    public String getReferences() {
        return references;
    }

    public void setReferences(String references) {
        this.references = references;
    }

    public String getCredits() {
        return credits;
    }

    public void setCredits(String credits) {
        this.credits = credits;
    }

    public Date getCreated() {
        return created;
    }

    public void setCreated(Date created) {
        this.created = created;
    }

    public Date getPublished() {
        return published;
    }

    public void setPublished(Date published) {
        this.published = published;
    }

    public Date getUpdated() {
        return updated;
    }

    public void setUpdated(Date updated) {
        this.updated = updated;
    }

    public String getVulnerableVersions() {
        return vulnerableVersions;
    }

    public void setVulnerableVersions(String vulnerableVersions) {
        this.vulnerableVersions = vulnerableVersions;
    }

    public String getPatchedVersions() {
        return patchedVersions;
    }

    public void setPatchedVersions(String patchedVersions) {
        this.patchedVersions = patchedVersions;
    }

    public String getSource() {
        return source;
    }

    public void setSource(String source) {
        this.source = source;
    }

    public void setSource(Source source) {
        this.source = source.name();
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getSubTitle() {
        return subTitle;
    }

    public void setSubTitle(String subTitle) {
        this.subTitle = subTitle;
    }

    /**
     * Setter for keeping the REST API backwards-compatible.
     * Will be removed in v5.
     *
     * @deprecated Use {@link #getCwes()} instead
     * @return The first {@link Cwe} returned by {@link #getCwes()}, or {@code null} if no {@link Cwe}s are set.
     */
    @JsonGetter("cwe")
    public Cwe getCwe() {
        if (cwes == null || cwes.isEmpty()) {
            return null;
        }

        return CweResolver.getInstance().lookup(cwes.get(0));
    }

    /**
     * Setter for keeping the REST API backwards-compatible.
     * Will be removed in v5.
     *
     * @deprecated Use {@link #setCwes(List)} instead
     * @param cwe The {@link Cwe} to set
     */
    @JsonSetter("cwe")
    public void setCwe(final Cwe cwe) {
        if (cwe != null) {
            setCwes(List.of(cwe.getCweId()));
        }
    }

    public List<Integer> getCwes() {
        return cwes;
    }

    public void setCwes(List<Integer> cwes) {
        if (cwes == null) {
            this.cwes = null;
        } else {
            final List<Integer> nonNullCwes = cwes.stream()
                    .filter(Objects::nonNull)
                    .toList();
            this.cwes = new ArrayList<>(nonNullCwes);
        }
    }

    public void addCwe(Integer cweId) {
        if (cweId == null) {
            return;
        }
        if (this.cwes == null) {
            this.cwes = new ArrayList<>();
        }
        this.cwes.add(cweId);
    }

    public void addCwe(Cwe cwe) {
        if (cwe == null) {
            return;
        }
        if (this.cwes == null) {
            this.cwes = new ArrayList<>();
            this.cwes.add(cwe.getCweId());
        } else {
            if (!this.cwes.contains(cwe.getCweId())) {
                this.cwes.add(cwe.getCweId());
            }
        }
    }

    public BigDecimal getCvssV2BaseScore() {
        return cvssV2BaseScore;
    }

    public void setCvssV2BaseScore(BigDecimal cvssV2BaseScore) {
        this.cvssV2BaseScore = cvssV2BaseScore;
    }

    public BigDecimal getCvssV2ImpactSubScore() {
        return cvssV2ImpactSubScore;
    }

    public void setCvssV2ImpactSubScore(BigDecimal cvssV2ImpactSubScore) {
        this.cvssV2ImpactSubScore = cvssV2ImpactSubScore;
    }

    public BigDecimal getCvssV2ExploitabilitySubScore() {
        return cvssV2ExploitabilitySubScore;
    }

    public void setCvssV2ExploitabilitySubScore(BigDecimal cvssV2ExploitabilitySubScore) {
        this.cvssV2ExploitabilitySubScore = cvssV2ExploitabilitySubScore;
    }

    public String getCvssV2Vector() {
        return cvssV2Vector;
    }

    public void setCvssV2Vector(String cvssV2Vector) {
        this.cvssV2Vector = cvssV2Vector;
    }

    public BigDecimal getCvssV3BaseScore() {
        return cvssV3BaseScore;
    }

    public void setCvssV3BaseScore(BigDecimal cvssV3BaseScore) {
        this.cvssV3BaseScore = cvssV3BaseScore;
    }

    public BigDecimal getCvssV3ImpactSubScore() {
        return cvssV3ImpactSubScore;
    }

    public void setCvssV3ImpactSubScore(BigDecimal cvssV3ImpactSubScore) {
        this.cvssV3ImpactSubScore = cvssV3ImpactSubScore;
    }

    public BigDecimal getCvssV3ExploitabilitySubScore() {
        return cvssV3ExploitabilitySubScore;
    }

    public void setCvssV3ExploitabilitySubScore(BigDecimal cvssV3ExploitabilitySubScore) {
        this.cvssV3ExploitabilitySubScore = cvssV3ExploitabilitySubScore;
    }

    public String getCvssV3Vector() {
        return cvssV3Vector;
    }

    public void setCvssV3Vector(String cvssV3Vector) {
        this.cvssV3Vector = cvssV3Vector;
    }

    public BigDecimal getEpssScore() {
        return epssScore;
    }

    public void setEpssScore(BigDecimal epssScore) {
        this.epssScore = epssScore;
    }

    public BigDecimal getEpssPercentile() {
        return epssPercentile;
    }

    public void setEpssPercentile(BigDecimal epssPercentile) {
        this.epssPercentile = epssPercentile;
    }

    public List<VulnerableSoftware> getVulnerableSoftware() {
        return vulnerableSoftware;
    }

    public void setVulnerableSoftware(List<VulnerableSoftware> vulnerableSoftware) {
        this.vulnerableSoftware = vulnerableSoftware;
    }

    public List<Component> getComponents() {
        return components;
    }

    public void setComponents(List<Component> components) {
        this.components = components;
    }

    public List<ServiceComponent> getServiceComponents() {
        return serviceComponents;
    }

    public void setServiceComponents(List<ServiceComponent> serviceComponents) {
        this.serviceComponents = serviceComponents;
    }

    public UUID getUuid() {
        return uuid;
    }

    public void setUuid(UUID uuid) {
        this.uuid = uuid;
    }

    public int getAffectedProjectCount() {
        return affectedProjectCount;
    }

    public void setAffectedProjectCount(int affectedProjectCount) {
        this.affectedProjectCount = affectedProjectCount;
    }

    public int getAffectedActiveProjectCount() {
        return affectedActiveProjectCount;
    }

    public void setAffectedActiveProjectCount(int affectedActiveProjectCount) {
        this.affectedActiveProjectCount = affectedActiveProjectCount;
    }

    public int getAffectedInactiveProjectCount() {
        return affectedInactiveProjectCount;
    }

    public void setAffectedInactiveProjectCount(int affectedInactiveProjectCount) {
        this.affectedInactiveProjectCount = affectedInactiveProjectCount;
    }

    public FindingAttribution getFindingAttribution() {
        return findingAttribution;
    }

    public void setFindingAttribution(FindingAttribution findingAttribution) {
        this.findingAttribution = findingAttribution;
    }

    public List<AffectedComponent> getAffectedComponents() {
        return affectedComponents;
    }

    public void setAffectedComponents(List<AffectedComponent> affectedComponents) {
        this.affectedComponents = affectedComponents;
    }

    public List<VulnerabilityAlias> getAliases() {
        return aliases;
    }

    public void setAliases(List<VulnerabilityAlias> aliases) {
        this.aliases = aliases;
    }

    public BigDecimal getOwaspRRLikelihoodScore() {
        return owaspRRLikelihoodScore;
    }

    public void setOwaspRRLikelihoodScore(BigDecimal owaspRRLikelihoodScore) {
        this.owaspRRLikelihoodScore = owaspRRLikelihoodScore;
    }

    public BigDecimal getOwaspRRTechnicalImpactScore() {
        return owaspRRTechnicalImpactScore;
    }

    public void setOwaspRRTechnicalImpactScore(BigDecimal owaspRRTechnicalImpactScore) {
        this.owaspRRTechnicalImpactScore = owaspRRTechnicalImpactScore;
    }

    public BigDecimal getOwaspRRBusinessImpactScore() {
        return owaspRRBusinessImpactScore;
    }

    public void setOwaspRRBusinessImpactScore(BigDecimal owaspRRBusinessImpactScore) {
        this.owaspRRBusinessImpactScore = owaspRRBusinessImpactScore;
    }

    public String getOwaspRRVector() {
        return owaspRRVector;
    }

    public void setOwaspRRVector(String owaspRRVector) {
        this.owaspRRVector = owaspRRVector;
    }
}
